<?php

/**
 * Maikuro Framework
 *
 * LICENSE:
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * The license is bundled with this package in the file LICENSE.txt.
 * You may also obtain a copy of the License at this URL:
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * @copyright  Copyright (c) 2010, Jonathan Hedrén
 * @author     Jonathan Hedrén <jonathan.hedren@gmail.com>
 * @license    http://www.apache.org/licenses/LICENSE-2.0 Apache License, Version 2.0
 */

class PDOSelectQuery
{
    /**
     *
     * @var string
     */
    protected $distinct;

    /**
     *
     * @var string
     */
    protected $columns;

    /**
     *
     * @var string
     */
    protected $from;

    /**
     *
     * @var array
     */
    protected $joins = array();

    /**
     *
     * @var string
     */
    protected $where;

    /**
     *
     * @var string
     */
    protected $groupBy;

    /**
     *
     * @var string
     */
    protected $having;

    /**
     *
     * @var string
     */
    protected $orderBy;

    /**
     *
     * @var string
     */
    protected $orderDir;

    /**
     *
     * @var string
     */
    protected $limit;

    /**
     *
     * @var PDO
     */
    private $pdo;

    /**
     *
     * @var array
     */
    private $values = array();

    /**
     *
     * @param ExtendedPDO $pdo
     */
    public function  __construct(ExtendedPDO $pdo)
    {
        $this->pdo = $pdo;
    }

    /**
     *
     * @return PDOSelectQuery
     */
    public function distinct()
    {
        $this->distinct = 'DISTINCT ';
        return $this;
    }

    /**
     *
     * @param array|string $columns
     * @return PDOSelectQuery
     */
    public function columns($columns)
    {
        if (!is_array($columns)) {
            $columns = func_get_args();
        }

        $this->columns = implode(', ', $columns);

        return $this;
    }

    /**
     *
     * @param string $table
     * @return PDOSelectQuery
     */
    public function from($table)
    {
        $this->from = ' FROM ' . $table;
        return $this;
    }

    /**
     *
     * @param string $table
     * @param string $constraint
     * @return PDOSelectQuery
     */
    public function leftJoin($table, $constraint)
    {
        $this->joins[] = ' LEFT OUTER JOIN ' . $table . ' ON ' . $constraint;
        return $this;
    }

    /**
     *
     * @param string $table
     * @param string $constraint
     * @return PDOSelectQuery
     */
    public function innerJoin($table, $constraint)
    {
        $this->joins[] = ' INNER JOIN ' . $table . ' ON ' . $constraint;
        return $this;
    }

    /**
     *
     * @param string $expression
     * @return PDOSelectQuery
     */
    public function where($expression)
    {
        $this->where = ' WHERE ' . $expression;
        return $this;
    }

    /**
     *
     * @param string $expression
     * @return PDOSelectQuery
     */
    public function andWhere($expression)
    {
        if ($this->where == null) {
            throw new Exception('AND {where expression} can only be applied if there already is an existing where expression.');
        }
        $this->where .= ' AND ' . $expression;
        return $this;
    }

    /**
     *
     * @param string $expression
     * @return PDOSelectQuery
     */
    public function orWhere($expression)
    {
        if ($this->where == null) {
            throw new Exception('OR {where expression} can only be applied if there already is an existing where expression.');
        }
        $this->where .= ' OR ' . $expression;
        return $this;
    }

    /**
     *
     * @param array|string $columns
     * @return PDOSelectQuery
     */
    public function groupBy($columns)
    {
        if (!is_array($columns)) {
            $columns = func_get_args();
        }
        $this->groupBy = ' GROUP BY ' . implode(', ', $columns);
        return $this;
    }


    /**
     *
     * @param string $expression
     * @return PDOSelectQuery
     */
    public function having($expression)
    {
        $this->having = ' HAVING ' . $expression;
        return $this;
    }

    /**
     *
     * @param array|string $columns
     * @return PDOSelectQuery
     */
    public function orderBy($columns)
    {
        if (!is_array($columns)) {
            $columns = func_get_args();
        }

        $this->orderBy = ' ORDER BY ' . implode(', ', $columns);
        return $this;
    }

    /**
     *
     * @return PDOSelectQuery
     */
    public function asc()
    {
        $this->orderDir = ' ASC';
        return $this;
    }

    /**
     *
     * @return PDOSelectQuery
     */
    public function desc()
    {
        $this->orderDir = ' DESC';
        return $this;
    }

    /**
     *
     * @param integer $limit
     * @param integer $offset
     * @return PDOSelectQuery
     */
    public function limit($limit, $offset = 0)
    {
        $this->limit = ' LIMIT ' . $limit . ' OFFSET ' . $offset;
        return $this;
    }

    /**
     *
     * @param string $className
     * @return array
     */
    public function fetchAllAs($className)
    {
        $statement = $this->pdo->prepareAndBindValues($this->toSql(), $this->values);
        $statement->execute();
        $result = $statement->fetchAll(PDO::FETCH_CLASS, $className);
        $statement->closeCursor();
        return $result;
    }

    /**
     *
     * @return array
     */
    public function fetchAll()
    {
        $statement = $this->pdo->prepareAndBindValues($this->toSql(), $this->values);
        $statement->execute();
        $result = $statement->fetchAll(PDO::FETCH_OBJ);
        $statement->closeCursor();
        return $result;
    }

    /**
     * Fetches the first row from the result set as an StdClass object. This
     * method will close the statement cursor after the row is fetched.
     *
     * @return StdClass
     */
    public function fetch()
    {
        $statement = $this->pdo->prepareAndBindValues($this->toSql(), $this->values);
        $statement->execute();
        $result = $statement->fetchObject();
        $statement->closeCursor();
        return $result;
    }

    /**
     *
     * @param string $className
     * @return object
     */
    public function fetchAs($className)
    {
        $statement = $this->pdo->prepareAndBindValues($this->toSql(), $this->values);
        $statement->execute();
        $result = $statement->fetchObject($className);
        $statement->closeCursor();
        return $result;
    }

    /**
     *
     * @param array|string $values
     * @return PDOSelectQuery
     */
    public function bindValues($values)
    {
        if (!is_array($values)) {
            $values = func_get_args();
        }
        $this->values = $values;
        return $this;
    }

    /**
     * Converts the PDOSelectQuery to an SQL string.
     *
     * @return string
     */
    public function toSql($withValues = false)
    {
        $sql = 'SELECT ';

        $sql .= $this->distinct;

        if ($this->columns == null) {
            $sql .= '* ';
        } else {
            $sql .= $this->columns;
        }

        if ($this->from == null) {
            throw new Exception('There must be a table to query.');
        }
        $sql .= $this->from;

        foreach ($this->joins as $join) {
           $sql .= $join;
        }

        $sql .= $this->where;

        if ($this->groupBy != null) {
            $sql .= $this->groupBy;
            $sql .= $this->having;
        }

        if ($this->orderBy != null) {
            $sql .= $this->orderBy;
            $sql .= $this->orderDir;
        }

        $sql .= $this->limit;

        if ($withValues) {
            $whitespaceValues = array();
            foreach ($this->values as $value) {
                $whitespaceValues[] = $value . ' ';
            }
            
            $sql = str_replace(array_fill(0, count($whitespaceValues), '? '), $whitespaceValues, $sql);
        }
        
        return $sql;
    }
}